winbrew_engines\windows\exe/
metadata.rs

1use anyhow::Result;
2use std::fs;
3use std::path::Path;
4use tracing::warn;
5
6use crate::windows_dep::installed::{UninstallEntry, uninstall_entries_matching};
7
8pub(super) enum NativeExeInstallMetadata {
9    QuietOnly(String),
10    QuietAndStandard {
11        quiet_uninstall_command: String,
12        uninstall_command: String,
13    },
14    StandardOnly(String),
15}
16
17pub(super) fn capture_native_exe_metadata(
18    package_name: &str,
19    install_dir: &Path,
20) -> Option<NativeExeInstallMetadata> {
21    capture_native_exe_metadata_with(package_name, install_dir, uninstall_entries_matching)
22}
23
24pub(super) fn capture_native_exe_metadata_with(
25    package_name: &str,
26    install_dir: &Path,
27    collect_entries: impl FnOnce(&str) -> Result<Vec<UninstallEntry>>,
28) -> Option<NativeExeInstallMetadata> {
29    let package_name = package_name.trim();
30    let mut best_match: Option<(u8, NativeExeInstallMetadata)> = None;
31    let mut saw_ambiguous_match = false;
32
33    let Ok(entries) = collect_entries(package_name) else {
34        return None;
35    };
36
37    for entry in entries {
38        if !entry.display_name.trim().eq_ignore_ascii_case(package_name) {
39            continue;
40        }
41
42        let install_location_exact = match entry.install_location.as_deref() {
43            Some(value) if !value.trim().is_empty() => {
44                if !same_install_location(Path::new(value), install_dir) {
45                    continue;
46                }
47
48                true
49            }
50            _ => false,
51        };
52
53        let candidate = match (
54            entry.quiet_uninstall_string.as_deref(),
55            entry.uninstall_string.as_deref(),
56        ) {
57            (Some(quiet_uninstall_command), Some(uninstall_command)) => Some((
58                native_exe_metadata_priority(install_location_exact, 3),
59                NativeExeInstallMetadata::QuietAndStandard {
60                    quiet_uninstall_command: quiet_uninstall_command.to_string(),
61                    uninstall_command: uninstall_command.to_string(),
62                },
63            )),
64            (Some(quiet_uninstall_command), None) => Some((
65                native_exe_metadata_priority(install_location_exact, 2),
66                NativeExeInstallMetadata::QuietOnly(quiet_uninstall_command.to_string()),
67            )),
68            (None, Some(uninstall_command)) => Some((
69                native_exe_metadata_priority(install_location_exact, 1),
70                NativeExeInstallMetadata::StandardOnly(uninstall_command.to_string()),
71            )),
72            (None, None) => None,
73        };
74
75        let Some((priority, metadata)) = candidate else {
76            continue;
77        };
78
79        match best_match.as_mut() {
80            Some((best_priority, best_metadata)) => {
81                if priority > *best_priority {
82                    *best_priority = priority;
83                    *best_metadata = metadata;
84                } else if priority == *best_priority {
85                    saw_ambiguous_match = true;
86                }
87            }
88            None => {
89                best_match = Some((priority, metadata));
90            }
91        }
92    }
93
94    if saw_ambiguous_match {
95        warn!(
96            package = package_name,
97            install_dir = %install_dir.display(),
98            "multiple native executable uninstall registry entries matched; using the best available metadata"
99        );
100    }
101
102    best_match.map(|(_, metadata)| metadata)
103}
104
105fn native_exe_metadata_priority(install_location_exact: bool, metadata_priority: u8) -> u8 {
106    if install_location_exact {
107        10 + metadata_priority
108    } else {
109        metadata_priority
110    }
111}
112
113pub(super) fn same_install_location(left: &Path, right: &Path) -> bool {
114    match (fs::canonicalize(left), fs::canonicalize(right)) {
115        (Ok(left), Ok(right)) => left == right,
116        _ => normalize_path_text(left) == normalize_path_text(right),
117    }
118}
119
120fn normalize_path_text(path: &Path) -> String {
121    path.to_string_lossy()
122        .replace('/', "\\")
123        .trim_end_matches('\\')
124        .to_ascii_lowercase()
125}